{# THE chat surface: thread, jump button, staged images, composer. The AI page's Chat section includes it and the drawer fetches it; there is one instance on a page at a time (the drawer empties while the Chat section shows), so the ids the scripts and the composer target stay unique. Needs: assistant, chat_path, stream_url, turn_defaults, messages, conversation_id, picker (the catalog is there to pick from), voice (how a spoken turn is heard, or none), approvals (the change queue's review list's URL, or none), voices (profiles and live calls: voice with a database). #} {% from "components/macros/icons.html" import icon %} {% from "components/macros/chat.html" import assistant_row, button_classes, conversation_field, voice_states %} {# A call's states, as the phone and the call bar both draw them. #} {% macro phone_states() %}{% call voice_states(record=True, work=True) %}{{ icon("phone", "w-4 h-4") }}{% endcall %}{% endmacro %}
{% if approvals %} {# Every card still waiting for the user, beside the thread, so a proposal is never lost with the conversation it was made in. #}
Waiting for your approval {# Fetched when first opened: most visits never open it. #}
{% endif %}
    {% include "partials/chat/transcript.html" %}
{# Staged images live in the script (they ride the stream request, one turn only); the chips are drawn from that list. #} {# The markup the script clones: a staged image's chip, and the image viewer's body for the modal. Styling lives here, not in JS. #} {% if voices %} {# A card drawn mid-call, which the call announces: held at the foot of the thread until the saved turn redraws the thread. #} {% endif %} {% if voice %} {# What the microphone is doing, read out as well as shown. The server's own refusals (nothing heard, transcription down) arrive as text. #} {% endif %}

{# Composer: the post appends the turn's bubbles; the script takes it from there. Enter sends, Shift+Enter inserts a newline. #}
{{ conversation_field(conversation_id) }} {# The chips are a row of their own above the box: beside it, in the drawer, they took the width the message needs. #}
{% if picker %}{% endif %} {% if voices %}{% endif %}
{# Not ``required``: a message may be images alone (the route sends the house line); Send stays disabled until there is text or an image. #} {% if voice %} {# The microphone: records while pressed on, then sends what it heard as the message (static/js/voice.js). data-state is what the assistant is doing; voice_states draws it. #} {% endif %} {% if voices %} {# A live call (routes/partials/chat_live.py): the server holds the engine's session and carries the audio over this mount's socket. #} {% endif %}
{% if voices %} {# In a call the composer row gives way to this: what the call is doing (voice_states), the engine, a timer and the running cost, mute and hang up. voice.js swaps the two and fills the figures. #} {% endif %}